package net.sitemorph.protostore; import com.google.common.collect.Lists; import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.FieldDescriptor; import com.google.protobuf.Descriptors.FieldDescriptor.Type; import com.google.protobuf.Message; import java.util.Collections; import java.util.Comparator; import java.util.EnumSet; import java.util.List; import java.util.UUID; /** * This store is simply intended to be used as an in memory cache or * buffer rather than a persisted store. This means that you must set all * required fields for the builder before it is passed. * * As with the other database backed storage the in memory store does support * read with fields set. * * The store supports filtering by urn or index field. Note that these * operations are O(N) as they iterate over the full data set. The store * supports a single order by field as well as named secondary indexes. Results * are stored in the sort order specified so will be returned in that order * whenever iterated over. * * Note that it is assumed that the urn field is a UUID string and will be set * as such when create is called, checking for uniqueness. * * If set up when creating the store support for vector clocks is also provided. * This is achieved by the use of 'reads' having a hinted vector clock field * which must store a signed long value which is used to signal updates to the * store. Vector values should not be set but will be required on update. This * means that a read / update can be checked to ensure that writes to a message * store are reflective of updates made. If two readers read concurrently one * will succeed in the write then the other, the second will throw an update * vector error due to clock skew. * * @author damien@sitemorph.net */ public class InMemoryStore<T extends Message> implements CrudStore<T> { private static final long INITIAL_VECTOR = 0; private FieldDescriptor urnField; private List<FieldDescriptor> indexes = Lists.newArrayList(); private List<T> data = Lists.newArrayList(); private Descriptor descriptor; private FieldDescriptor sortField = null; private SortOrder direction = SortOrder.ASCENDING; private FieldDescriptor vectorField = null; private InMemoryStore() {} @Override public synchronized T create(Message.Builder builder) throws CrudException { // find a urn for the new object UUID urn = UUID.randomUUID(); CrudIterator<T> priors = new FilteringDataIterator<T>(data, urnField, urn); while (priors.hasNext()) { urn = UUID.randomUUID(); priors = new FilteringDataIterator<T>(data, urnField, urn); } builder.setField(urnField, urn.toString()); if (null != vectorField) { setInitialVector(builder, vectorField); } T newValue = (T) builder.build(); int insertAt; if (null != sortField) { insertAt = Collections.binarySearch(data, newValue, new InMemoryComparator<T>(sortField, direction)); if (0 > insertAt) { // if not exactly found then will be inserted. insertAt = -(insertAt) - 1; } // otherwise will be inserted at the position of the first matching // value according to the insert order. } else { insertAt = data.size(); } // TODO 20131111 Consider a scan to remove stale objects based on urn data.add(insertAt, newValue); return newValue; } @Override public synchronized CrudIterator<T> read(Message.Builder builder) throws CrudException { if (builder.hasField(urnField)) { // read based on the urn field return new FilteringDataIterator<T>(Lists.newArrayList(data), urnField, builder.getField(urnField)); } // iterate over the index fields for (FieldDescriptor field : indexes) { if (builder.hasField(field)) { return new FilteringDataIterator<T>(Lists.newArrayList(data), field, builder.getField(field)); } } // read all data return new AllDataIterator<T>(Lists.newArrayList(data)); } /** * Helper to allow updating of data outside of the normal vector clock / update * operation. * * @param updated item to overwrite a value already stored. */ void refresh(T updated) { int index = -1; for (int i = 0; i < data.size(); i++) { if (data.get(i).getField(urnField).equals(updated.getField(urnField))) { index = i; break; } } data.set(index, updated); } void add(T item) { data.add(item); Collections.sort(data, new InMemoryComparator<T>(sortField, direction)); } @Override public synchronized T update(Message.Builder builder) throws CrudException { if (!builder.hasField(urnField)) { throw new IllegalArgumentException("Update provided does not include " + "a value for the urn field"); } Object updateUrn = builder.getField(urnField); for (int i = 0; i < data.size(); i++) { T old = data.get(i); // if this is our message to update if (old.getField(urnField).equals(updateUrn)) { if (null != vectorField) { if (!builder.hasField(vectorField)) { throw new MessageVectorException("Update is missing clock vector"); } if (!builder.getField(vectorField).equals(old.getField(vectorField))) { throw new MessageVectorException("Update vector is out of date"); } updateVector(builder, vectorField); } T result = (T) builder.build(); data.set(i, result); // sort the data in case the update order changed Collections.sort(data, new InMemoryComparator<T>(sortField, direction)); return result; } } throw new MessageNotFoundException("Update passed message that was not " + "stored. Update not possible"); } @Override public synchronized void delete(T message) throws CrudException { // TODO 20131111 Implement based on urn column for (int i = 0; i < data.size(); i++) { T old = data.get(i); if (old.getField(urnField).equals(message.getField(urnField))) { if (null != vectorField) { if (!message.getField(vectorField).equals(old.getField(vectorField))) { throw new MessageVectorException("Update failed due to vector " + "mismatch"); } } data.remove(i); return; } } throw new MessageNotFoundException("Failed to delete missing message"); } @Override public void close() throws CrudException { // Do nothing as the close operation is intended to release access to the // store specifically not clear it's contents. Multiple references may be // held to the in memory store. Once all are released then GC will collect } static void updateVector(Message.Builder builder, FieldDescriptor vectorField) { Object current = builder.getField(vectorField); if (null == current) { throw new RuntimeException("Message clock vector data missing"); } if (!(current instanceof Long)) { throw new RuntimeException("Message clock vector data type error"); } Long value = (Long)current; if (value == Long.MAX_VALUE) { value = INITIAL_VECTOR; } else { value += 1L; } builder.setField(vectorField, value); } static void setInitialVector(Message.Builder builder, FieldDescriptor vectorField) { builder.setField(vectorField, INITIAL_VECTOR); } public static class Builder<M extends Message> { private InMemoryStore<M> result; private Message.Builder prototype; private static final EnumSet<Type> INTEGRALS = EnumSet.of( Type.INT64, Type.UINT64, Type.FIXED64, Type.SFIXED64, Type.SINT64); public Builder() { result = new InMemoryStore<M>(); } public Builder<M> setPrototype(Message.Builder prototype) { this.prototype = prototype; result.descriptor = prototype.getDescriptorForType(); return this; } /** * Set the urn field for the store based on the name of the field. * @param fieldName to find. * @return builder * @throws IllegalStateException if you have not set the prototype first * @throws IllegalArgumentException if you pass a name not found. */ public Builder<M> setUrnField(String fieldName) { if (null == prototype) { throw new IllegalStateException("Can't choose field based on name " + "because no prototype has been set"); } Descriptor descriptor = prototype.getDescriptorForType(); for (FieldDescriptor field : descriptor.getFields()) { if (field.getName().equals(fieldName)) { result.urnField = field; return this; } } throw new IllegalArgumentException("Supplied field name " + fieldName + " did not match any descriptor field names"); } public Builder<M> addIndexField(String fieldName) { if (null == prototype) { throw new IllegalStateException("Can't add index field as no " + "prototype has been set"); } Descriptor descriptor = prototype.getDescriptorForType(); for (FieldDescriptor field : descriptor.getFields()) { if (field.getName().equals(fieldName)) { result.indexes.add(field); return this; } } throw new IllegalArgumentException("Supplied field name " + fieldName + "did not match any field descriptor field names"); } public Builder<M> setVectorField(String fieldName) { if (null == prototype) { throw new IllegalStateException("Can't set vector field as no " + "prototype has been set"); } Descriptor descriptor = prototype.getDescriptorForType(); for (FieldDescriptor field : descriptor.getFields()) { if (field.getName().equals(fieldName)) { if (!INTEGRALS.contains(field.getType())) { throw new IllegalArgumentException("Can't use a vector clock " + "on a non long field"); } result.vectorField = field; return this; } } throw new IllegalArgumentException("Can't find the requested vector " + "clock field: " + fieldName); } public Builder<M> setSortOrder(String fieldName, SortOrder direction) { if (null == prototype) { throw new IllegalStateException("Can't set sort order field as no " + "prototype has been set"); } Descriptor descriptor = prototype.getDescriptorForType(); for (FieldDescriptor field : descriptor.getFields()) { if (field.getName().equals(fieldName)) { switch (field.getType()) { case BYTES: case ENUM: case GROUP: case MESSAGE: throw new IllegalArgumentException("Field " + fieldName + " has complex type " + field.getType().name() + " which " + "can't be used as a sort field"); } result.sortField = field; result.direction = direction; return this; } } throw new IllegalArgumentException("Supplied field name " + fieldName + "did not match any field descriptor field names."); } public CrudStore<M> build() { if (null == result.sortField) { result.sortField = result.urnField; } return result; } } private static class InMemoryComparator<L extends Message> implements Comparator<L> { private final FieldDescriptor sortField; private final SortOrder direction; InMemoryComparator(FieldDescriptor sortField, SortOrder direction) { this.sortField = sortField; this.direction = direction; } @Override public int compare(L left, L right) { Object leftValue = left.getField(sortField); if (!(leftValue instanceof Comparable)) { throw new IllegalArgumentException("Underlying type is not " + "comparable. Please check your configuration. for sort field " + sortField.getName()); } Object rightValue = right.getField(sortField); if (!(rightValue instanceof Comparable)) { throw new IllegalArgumentException("Underlying type is not " + "comparable. please check your configuration for sort field " + sortField.getName()); } if (SortOrder.ASCENDING == direction) { return ((Comparable) leftValue).compareTo(rightValue); } else { return ((Comparable) rightValue).compareTo(leftValue); } } } }